import warnings
import pandas as pd
import numpy as np
import scipy.optimize as opt


def expected_rtns(monthly_rtns, freq=12):
    """
    To calculate annualized returns for selected asset classes based on historical monthly returns.
    :param monthly_rtns: pd.DataFrame
                         historical monthly returns during the time selected.Row for date, columns for asset's code.
    :param freq: int
                 frequancy of 12 means to annualize average monthly return.
    :return: pd.Series
             expected returns (annualized) for each asset class
    """
    if not isinstance(monthly_rtns, pd.DataFrame):
        warnings.warn("Monthly returns is not in Dataframe", RuntimeWarning)
    return monthly_rtns.mean()*freq


def ema_historical_return(monthly_rtns, freq=12, span=36):
    """
    Calculate the exponentially-weighted mean of (monthly) historical returns, giving
    higher weight to the more recent data.
    :param monthly_rtns: pd.DataFrame
                         Historical monthly returns during the time selected. Row for date, columns for asset's code.
    :param freq: int
                 Number of periods in a year. (12 month per year)
    :param span: int
                 The time-span for the EMA, defaults to 36-month EMA.
    :return: pd.Series
             Annualised exponentially-weighted mean return of each asset
    """
    if not isinstance(monthly_rtns, pd.DataFrame):
        warnings.warn("Monthly returns is not in Dataframe", RuntimeWarning)
    return monthly_rtns.ewm(span=span).mean().iloc[-1] * freq


def port_return(w, rtns):
    """
    To calculate the return of a portfolio.
    :param w: np.ndarray
                    Asset weights of a portfolio.
    :param exp_rtns: pd.Series
                     Expected return for each asset.
    :return: float
             Portfolio return
    """
    return np.dot(w, rtns)


def neg_port_return(w, rtns):
    """
    To calculate the negative return of a portfolio.
    This is for constructing optimizer's minimize objective function
    :param w: np.ndarray
                    Asset weights of a portfolio.
    :param rtns: pd.Series
                     Expected return for each asset.
    :return: float
             Portfolio return
    """
    return -port_return(w, rtns)


def get_portfolio_var(w, covmat):
    # function that calculates portfolio risk
    w = np.matrix(w)
    return np.dot(np.dot(w, covmat), w.T)


def sharpe(weights, exp_rtns, cov_mat, risk_free_rate=0.15):
        """
        To calculate Sharpe ratio of a portfolio
        :param weights: np.ndarray
                        Asset weights of a portfolio.
        :param exp_rtns: expected return of each asset
        :type exp_rtns: pd.Series
        :param cov_mat: pd.DataFrame
                        Covariance matrix of asset returns.
        :param risk_free_rate: float
                               Market risk-free rate, defaults to 0.15, one-year savings fix interest rate at bank.
        :return: float
                 Sharpe ratio
        """
        mu = np.dot(weights, exp_rtns)
        sigma = np.sqrt(np.dot(np.dot(weights, cov_mat), weights.T))
        return (mu - risk_free_rate) / sigma


def neg_sharpe(weights, exp_rtns, cov_mat, risk_free_rate=0.15):
    """
    To calculate  negative Sharpe ratio of a portfolio.
    This is for constructing optimizer's minimize objective function
    :param weights: np.ndarray
                    Asset weights of a portfolio.
    :param exp_rtns: expected return of each asset
    :type exp_rtns: pd.Series
    :param cov_mat: pd.DataFrame
                       Covariance matrix of asset returns.
    :param risk_free_rate: float
                           Market risk-free rate, defaults to 0.15, one-year savings fix interest rate at bank.
    :return: float
             Sharpe ratio
    """
    mu = np.dot(weights, exp_rtns)
    sigma = np.sqrt(np.dot(np.dot(weights, cov_mat), weights.T))
    return -(mu - risk_free_rate) / sigma


def max_sharpe(exprtns, covmat):
    """
    Compute a max-sharpe portfolio.
    :return: asset weights for the maximized sharpe ratio portfolio
    :rtype: dict
    """
    weights = np.random.random(len(exprtns.index))
    weights /= np.sum(weights)
    rnoa = range(len(exprtns.index))
    # print("This is initial guess weights: ", weights)

    args = (covmat, exprtns)
    bnds = tuple((0.0, 1.0) for _ in rnoa)
    cons = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
    sol = opt.minimize(
        neg_sharpe,
        x0=weights,
        args=args,
        method="SLSQP",
        bounds=bnds,
        constraints=cons
    )
    weights = pd.Series(sol["x"])
    print("This is optimized weight using Max-Sharpe: ", sol["x"])
    return dict(zip(exprtns.index, weights))
